如何编写Quantumult

您所在的位置:网站首页 quantumult x 脚本 如何编写Quantumult

如何编写Quantumult

#如何编写Quantumult| 来源: 网络整理| 查看: 265

最近对脚本开发比较感兴趣,通过JavaScript编写脚本,不仅可以加深你对JavaScript使用的熟练程度,更甚可以锻炼你的逻辑能力。

IOS上面有一些可以定时运行js脚本的工具,这些工具可以实现通过js定时京东签到,漫画签到等一系列的功能。Quantumult-X就是其中的一个软件,我知道的还有Loon、Surge不过后两者我是完全没有使用过。

奇怪的是Quantumult-X这种软件,我目前没有找到官方的API文档,翻来覆去折腾了好久,还是决定直接看一下别人写的源码,其中我参考的是京东签到这个脚本的源代码。

读这种比较长的源码一定不要在网页上面阅读,因为没有代码定位,你可以将它拷贝到本地,通过vscode这一类的编辑器打开阅读,其中它有一个比较重要的方法就是。

注:本篇文章不适合对JavaScript没有了解的小白阅读,同时阅读本篇文章之前,相信你已经知道Quantumult-X的基础用法,并且已经可以正常使用别人的开源脚本。

1. 工具函数

下面是脚本作者封装的工具函数,里面实现了http请求、消息提醒、警告、数据持久化储存。

可以大致过一遍这些源代码,在后面的文章会单独的将上面提到的功能进行讲解。

/** * 工具类 * @return {{read: ((function(*=): (*|null|undefined))|*), isRequest: boolean, isLoon: boolean, isQuanX: boolean, isNode: boolean, done: ((function(*=): (*|undefined))|*), notify: notify, isSurge: boolean, post: post, AnError: (function(*, *=, *=, *=, *): void), get: get, time: (function(): void), isJSBox: boolean, write: ((function(*=, *=): (*|undefined))|*)}} */ export function nobyda() { const start = Date.now(); // 判断是否是重写 const isRequest = typeof $request != "undefined"; // 判断是否是Surge const isSurge = typeof $httpClient != "undefined"; // 判断是否是QuanX const isQuanX = typeof $task != "undefined"; // 判断是否是Loon const isLoon = typeof $loon != "undefined"; // 判断是否是JSBox const isJSBox = typeof $app != "undefined" && typeof $http != "undefined"; // 判断是否是Node环境 const isNode = typeof require == "function" && !isJSBox; const NodeSet = "CookieSet.json"; /** * 引入Nodejs中的request模块和fs模块 * @type {{request: *, fs: module:fs}|null} */ const node = (() => { if (isNode) { const request = require("request"); const fs = require("fs"); return ({ request, fs }); } else { return null; } })(); /** * 提示信息 * @param {string} title 标题 * @param {string} subtitle 副标题 * @param {string} message 提示信息 * @param {*} rawopts 设置 */ const notify = (title, subtitle, message, rawopts) => { const Opts = (rawopts) => { //Modified from https://github.com/chavyleung/scripts/blob/master/Env.js if (!rawopts) return rawopts; switch (typeof rawopts) { case "string": return isLoon ? rawopts : isQuanX ? { "open-url": rawopts, } : isSurge ? { url: rawopts, } : undefined; case "object": if (isLoon) { let openUrl = rawopts.openUrl || rawopts.url || rawopts["open-url"]; let mediaUrl = rawopts.mediaUrl || rawopts["media-url"]; return { openUrl, mediaUrl, }; } else if (isQuanX) { let openUrl = rawopts["open-url"] || rawopts.url || rawopts.openUrl; let mediaUrl = rawopts["media-url"] || rawopts.mediaUrl; return { "open-url": openUrl, "media-url": mediaUrl, }; } else if (isSurge) { let openUrl = rawopts.url || rawopts.openUrl || rawopts["open-url"]; return { url: openUrl, }; } break; default: return undefined; } }; console.log(`${ title }\n${ subtitle }\n${ message }`); if (isQuanX) $notify(title, subtitle, message, Opts(rawopts)); if (isSurge) $notification.post(title, subtitle, message, Opts(rawopts)); if (isJSBox) $push.schedule({ title: title, body: subtitle ? subtitle + "\n" + message : message }); }; // 将获得的cookies信息储存起来 const write = (value, key) => { if (isQuanX) return $prefs.setValueForKey(value, key); if (isSurge) return $persistentStore.write(value, key); if (isNode) { try { if (!node.fs.existsSync(NodeSet)) node.fs.writeFileSync(NodeSet, JSON.stringify({})); const dataValue = JSON.parse(node.fs.readFileSync(NodeSet)); if (value) dataValue[key] = value; if (!value) delete dataValue[key]; return node.fs.writeFileSync(NodeSet, JSON.stringify(dataValue)); } catch (er) { return AnError("Node.js持久化写入", null, er); } } if (isJSBox) { if (!value) return $file.delete(`shared://${ key }.txt`); return $file.write({ data: $data({ string: value }), path: `shared://${ key }.txt` }); } }; // 将获取的cookies信息读出来 const read = (key) => { if (isQuanX) return $prefs.valueForKey(key); if (isSurge) return $persistentStore.read(key); if (isNode) { try { if (!node.fs.existsSync(NodeSet)) return null; const dataValue = JSON.parse(node.fs.readFileSync(NodeSet)); return dataValue[key]; } catch (er) { return AnError("Node.js持久化读取", null, er); } } if (isJSBox) { if (!$file.exists(`shared://${ key }.txt`)) return null; return $file.read(`shared://${ key }.txt`).string; } }; const adapterStatus = (response) => { if (response) { if (response.status) { response["statusCode"] = response.status; } else if (response.statusCode) { response["status"] = response.statusCode; } } return response; }; // get请求 const get = (options, callback) => { options.headers["User-Agent"] = "JD4iPhone/167169 (iPhone; iOS 13.4.1; Scale/3.00)"; if (isQuanX) { if (typeof options == "string") options = { url: options }; options["method"] = "GET"; //options["opts"] = { // "hints": false //} $task.fetch(options).then(response => { callback(null, adapterStatus(response), response.body); }, reason => callback(reason.error, null, null)); } if (isSurge) { options.headers["X-Surge-Skip-Scripting"] = false; $httpClient.get(options, (error, response, body) => { callback(error, adapterStatus(response), body); }); } if (isNode) { node.request(options, (error, response, body) => { callback(error, adapterStatus(response), body); }); } if (isJSBox) { if (typeof options == "string") options = { url: options }; options["header"] = options["headers"]; options["handler"] = function (resp) { let error = resp.error; if (error) error = JSON.stringify(resp.error); let body = resp.data; if (typeof body == "object") body = JSON.stringify(resp.data); callback(error, adapterStatus(resp.response), body); }; $http.get(options); } }; // post请求 const post = (options, callback) => { options.headers["User-Agent"] = "JD4iPhone/167169 (iPhone; iOS 13.4.1; Scale/3.00)"; if (options.body) options.headers["Content-Type"] = "application/x-www-form-urlencoded"; if (isQuanX) { if (typeof options == "string") options = { url: options }; options["method"] = "POST"; $task.fetch(options).then(response => { callback(null, adapterStatus(response), response.body); }, reason => callback(reason.error, null, null)); } if (isSurge) { options.headers["X-Surge-Skip-Scripting"] = false; $httpClient.post(options, (error, response, body) => { callback(error, adapterStatus(response), body); }); } if (isNode) { node.request.post(options, (error, response, body) => { callback(error, adapterStatus(response), body); }); } if (isJSBox) { if (typeof options == "string") options = { url: options }; options["header"] = options["headers"]; options["handler"] = function (resp) { let error = resp.error; if (error) error = JSON.stringify(resp.error); let body = resp.data; if (typeof body == "object") body = JSON.stringify(resp.data); callback(error, adapterStatus(resp.response), body); }; $http.post(options); } }; // 异常信息 const AnError = (name, keyname, er, resp, body) => { if (typeof (merge) != "undefined" && keyname) { if (!merge[keyname].notify) { merge[keyname].notify = `${ name }: 异常, 已输出日志 ‼️`; } else { merge[keyname].notify += `\n${ name }: 异常, 已输出日志 ‼️ (2)`; } merge[keyname].error = 1; } return console.log(`\n‼️${ name }发生错误\n‼️名称: ${ er.name }\n‼️描述: ${ er.message }${ JSON.stringify(er).match(/"line"/) ? `\n‼️行列: ${ JSON.stringify(er) }` : `` }${ resp && resp.status ? `\n‼️状态: ${ resp.status }` : `` }${ body ? `\n‼️响应: ${ resp && resp.status != 503 ? body : `Omit.` }` : `` }`); }; // 总共用时 const time = () => { const end = ((Date.now() - start) / 1000).toFixed(2); return console.log("\n签到用时: " + end + " 秒"); }; // 关闭请求 const done = (value = {}) => { if (isQuanX) return $done(value); if (isSurge) isRequest ? $done(value) : $done(); }; return { AnError, isRequest, isJSBox, isSurge, isQuanX, isLoon, isNode, notify, write, read, get, post, time, done }; } 2. 分析 2.1 判断环境

其实大部分代码我都已经在阅读源码的时候标上了注释,源码不难理解,首先进入代码一开始就是判断各种环境:

// 判断是否是重写 const isRequest = typeof $request != "undefined"; // 判断是否是Surge const isSurge = typeof $httpClient != "undefined"; // 判断是否是QuanX const isQuanX = typeof $task != "undefined"; // 判断是否是Loon const isLoon = typeof $loon != "undefined"; // 判断是否是JSBox const isJSBox = typeof $app != "undefined" && typeof $http != "undefined"; // 判断是否是Node环境 const isNode = typeof require == "function" && !isJSBox;

该作者的这个脚本不仅仅可以用在QuanX上面,还可以在Surge、Loon、JSBox甚至是Node环境上运行。

2.2 提示信息

该方法封装了消息提示功能,就跟微信来消息的弹框一样,如果你没有给该软件提示权限则收不到相关的消息提示。

/** * 提示信息 * @param {string} title 标题 * @param {string} subtitle 副标题 * @param {string} message 提示信息 * @param {*} rawopts 设置 */ const notify = (title, subtitle, message, rawopts) => { const Opts = (rawopts) => { //Modified from https://github.com/chavyleung/scripts/blob/master/Env.js if (!rawopts) return rawopts; switch (typeof rawopts) { case "string": return isLoon ? rawopts : isQuanX ? { "open-url": rawopts, } : isSurge ? { url: rawopts, } : undefined; case "object": if (isLoon) { let openUrl = rawopts.openUrl || rawopts.url || rawopts["open-url"]; let mediaUrl = rawopts.mediaUrl || rawopts["media-url"]; return { openUrl, mediaUrl, }; } else if (isQuanX) { let openUrl = rawopts["open-url"] || rawopts.url || rawopts.openUrl; let mediaUrl = rawopts["media-url"] || rawopts.mediaUrl; return { "open-url": openUrl, "media-url": mediaUrl, }; } else if (isSurge) { let openUrl = rawopts.url || rawopts.openUrl || rawopts["open-url"]; return { url: openUrl, }; } break; default: return undefined; } }; console.log(`${ title }\n${ subtitle }\n${ message }`); if (isQuanX) $notify(title, subtitle, message, Opts(rawopts)); if (isSurge) $notification.post(title, subtitle, message, Opts(rawopts)); if (isJSBox) $push.schedule({ title: title, body: subtitle ? subtitle + "\n" + message : message }); }; 2.3 异常信息

脚本中封装的异常信息提示方法。

// 异常信息 const AnError = (name, keyname, er, resp, body) => { if (typeof (merge) != "undefined" && keyname) { if (!merge[keyname].notify) { merge[keyname].notify = `${ name }: 异常, 已输出日志 ‼️`; } else { merge[keyname].notify += `\n${ name }: 异常, 已输出日志 ‼️ (2)`; } merge[keyname].error = 1; } return console.log(`\n‼️${ name }发生错误\n‼️名称: ${ er.name }\n‼️描述: ${ er.message }${ JSON.stringify(er).match(/"line"/) ? `\n‼️行列: ${ JSON.stringify(er) }` : `` }${ resp && resp.status ? `\n‼️状态: ${ resp.status }` : `` }${ body ? `\n‼️响应: ${ resp && resp.status != 503 ? body : `Omit.` }` : `` }`); }; 2.4 请求

在脚本的编写中因为要获取第三方网站的信息,或者模拟请求,所以需要使用到http请求,由于各个软件的请求方式有些许不同,所以该脚本中封装了一个get以及post请求,进行了差异化处理,直接调用这两个方法就可以进行http请求。

// get请求 const get = (options, callback) => { options.headers["User-Agent"] = "JD4iPhone/167169 (iPhone; iOS 13.4.1; Scale/3.00)"; if (isQuanX) { if (typeof options == "string") options = { url: options }; options["method"] = "GET"; //options["opts"] = { // "hints": false //} $task.fetch(options).then(response => { callback(null, adapterStatus(response), response.body); }, reason => callback(reason.error, null, null)); } if (isSurge) { options.headers["X-Surge-Skip-Scripting"] = false; $httpClient.get(options, (error, response, body) => { callback(error, adapterStatus(response), body); }); } if (isNode) { node.request(options, (error, response, body) => { callback(error, adapterStatus(response), body); }); } if (isJSBox) { if (typeof options == "string") options = { url: options }; options["header"] = options["headers"]; options["handler"] = function (resp) { let error = resp.error; if (error) error = JSON.stringify(resp.error); let body = resp.data; if (typeof body == "object") body = JSON.stringify(resp.data); callback(error, adapterStatus(resp.response), body); }; $http.get(options); } }; // post请求 const post = (options, callback) => { options.headers["User-Agent"] = "JD4iPhone/167169 (iPhone; iOS 13.4.1; Scale/3.00)"; if (options.body) options.headers["Content-Type"] = "application/x-www-form-urlencoded"; if (isQuanX) { if (typeof options == "string") options = { url: options }; options["method"] = "POST"; $task.fetch(options).then(response => { callback(null, adapterStatus(response), response.body); }, reason => callback(reason.error, null, null)); } if (isSurge) { options.headers["X-Surge-Skip-Scripting"] = false; $httpClient.post(options, (error, response, body) => { callback(error, adapterStatus(response), body); }); } if (isNode) { node.request.post(options, (error, response, body) => { callback(error, adapterStatus(response), body); }); } if (isJSBox) { if (typeof options == "string") options = { url: options }; options["header"] = options["headers"]; options["handler"] = function (resp) { let error = resp.error; if (error) error = JSON.stringify(resp.error); let body = resp.data; if (typeof body == "object") body = JSON.stringify(resp.data); callback(error, adapterStatus(resp.response), body); }; $http.post(options); } }; // 关闭请求 const done = (value = {}) => { if (isQuanX) return $done(value); if (isSurge) isRequest ? $done(value) : $done(); }; 2.5 数据读取

该脚本针对数据持久化储存进行了封装,可以将你获取到的Cookie信息存储下来,就不用每次运行脚本时都需要填写Cookie信息。

// 将获得的cookies信息储存起来 const write = (value, key) => { if (isQuanX) return $prefs.setValueForKey(value, key); if (isSurge) return $persistentStore.write(value, key); if (isNode) { try { if (!node.fs.existsSync(NodeSet)) node.fs.writeFileSync(NodeSet, JSON.stringify({})); const dataValue = JSON.parse(node.fs.readFileSync(NodeSet)); if (value) dataValue[key] = value; if (!value) delete dataValue[key]; return node.fs.writeFileSync(NodeSet, JSON.stringify(dataValue)); } catch (er) { return AnError("Node.js持久化写入", null, er); } } if (isJSBox) { if (!value) return $file.delete(`shared://${ key }.txt`); return $file.write({ data: $data({ string: value }), path: `shared://${ key }.txt` }); } }; // 将获取的cookies信息读出来 const read = (key) => { if (isQuanX) return $prefs.valueForKey(key); if (isSurge) return $persistentStore.read(key); if (isNode) { try { if (!node.fs.existsSync(NodeSet)) return null; const dataValue = JSON.parse(node.fs.readFileSync(NodeSet)); return dataValue[key]; } catch (er) { return AnError("Node.js持久化读取", null, er); } } if (isJSBox) { if (!$file.exists(`shared://${ key }.txt`)) return null; return $file.read(`shared://${ key }.txt`).string; } }; 3. 重写

写过脚本或者爬虫的人应该都比较清楚,http为无状态请求,也就是后端并不知道请求之前用户进行了什么操作,如果服务器要识别某一请求为哪个用户发出来的,现在最主流的有两种办法,一种是token,一种是Cookie。

而从该脚本中可以得知,京东明显是使用了Cookie判断用户信息,那么如何使用脚本来获取cookie信息呢?

根据源代码,如果const isRequest = typeof $request != "undefined";那么该脚本即为重写。

那么我们只需要着重观察哪儿有调用isRequest。

if (DeleteCookie) { if ($nobyda.read(EnvInfo) || $nobyda.read(EnvInfo2)) { $nobyda.write("", EnvInfo); $nobyda.write("", EnvInfo2); $nobyda.notify("京东Cookie清除成功 !", "", "请手动关闭脚本内\"DeleteCookie\"选项"); $nobyda.done(); return; } $nobyda.notify("脚本终止", "", "未关闭脚本内\"DeleteCookie\"选项 ‼️"); $nobyda.done(); return; } else if ($nobyda.isRequest) { // 如果为重写,那么就执行GetCookie函数 GetCookie(); return; }

可以看到在$nobyda.isRequest为true时调用了GetCookie()函数,于是我们就着重分析GetCookie()函数。

// 自动获取cookie方法 function GetCookie() { try { if ($request.headers && $request.url.match(/api\.m\.jd\.com.*=signBean/)) { var CV = $request.headers["Cookie"]; if (CV.match(/pt_key=.+?;/) && CV.match(/pt_pin=.+?;/)) { var CookieValue = CV.match(/pt_key=.+?;/)[0] + CV.match(/pt_pin=.+?;/)[0]; var CK1 = $nobyda.read("CookieJD"); var CK2 = $nobyda.read("CookieJD2"); var AccountOne = CK1 ? CK1.match(/pt_pin=.+?;/) ? CK1.match(/pt_pin=(.+?);/)[1] : null : null; var AccountTwo = CK2 ? CK2.match(/pt_pin=.+?;/) ? CK2.match(/pt_pin=(.+?);/)[1] : null : null; var UserName = CookieValue.match(/pt_pin=(.+?);/)[1]; var DecodeName = decodeURIComponent(UserName); if (!AccountOne || UserName == AccountOne) { var CookieName = " [账号一] "; var CookieKey = "CookieJD"; } else if (!AccountTwo || UserName == AccountTwo) { var CookieName = " [账号二] "; var CookieKey = "CookieJD2"; } else { $nobyda.notify("更新京东Cookie失败", "非历史写入账号 ‼️", "请开启脚本内\"DeleteCookie\"以清空Cookie ‼️"); return; } } else { $nobyda.notify("写入京东Cookie失败", "", "请查看脚本内说明, 登录网页获取 ‼️"); return; } const RA = $nobyda.read(CookieKey); if (RA == CookieValue) { console.log(`\n用户名: ${ DecodeName }\n与历史京东${ CookieName }Cookie相同, 跳过写入 ⚠️`); } else { const WT = $nobyda.write(CookieValue, CookieKey); $nobyda.notify(`用户名: ${ DecodeName }`, ``, `${ RA ? `更新` : `写入` }京东${ CookieName }Cookie${ WT ? `成功 🎉` : `失败 ‼️` }`); } } else if ($request.url === "http://www.apple.com/") { $nobyda.notify("京东签到", "", "类型错误, 手动运行请选择上下文环境为Cron ⚠️"); } else { $nobyda.notify("京东签到", "写入Cookie失败", "请检查匹配URL或配置内脚本类型 ⚠️"); } } catch (eor) { $nobyda.write("", "CookieJD"); $nobyda.write("", "CookieJD2"); $nobyda.notify("写入京东Cookie失败", "", "已尝试清空历史Cookie, 请重试 ⚠️"); console.log(`\n写入京东Cookie出现错误 ‼️\n${ JSON.stringify(eor) }\n\n${ eor }\n\n${ JSON.stringify($request.headers) }\n`); } finally { $nobyda.done(); } }

因为Quantumult-X的重写功能为访问到指定的url就可以触发脚本,根据该脚本来看,几个软件的重写方法没有什么差异化,都是使用的$request对象,而Node环境下无法自动获取Cookie,必须进行手动填写。

4. 最后

我找了好久Quantumult-X都没有提供官方文档,所以我并不清楚它的API,不过从上面的脚本来看,大致分为下面几个API:

$prefs:持久化数据存储(读取和写入)。 $task:网络请求。 $done:请求完毕时需要调用。 $request:重写网络请求,用来获取请求中的Cookie等,甚至可以用来篡改响应体。 $notify:弹框提示信息。

看了一下该脚本封装的还是比较全的,几乎可能用到的方法都封装进去了,同时我尝试将脚本通过webpack进行压缩,事实证明即使经过webpack压缩,该脚本依然是可以正常使用。也就是说,在编写脚本的时候可以通过webpack引入一些第三方工具类。

目前来说通过webpack压缩脚本仅仅只有一点缺陷,就是在分享脚本时,别人无法直接阅读到你的源代码,而无法进行修改(修改难度高),不过如果你不想暴露自己的脚本源代码,通过webpack压缩是一个非常不错的选择。



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3